/* 
 * Block Cache Layer
 * 
 * cache.h
 *
 * Cache of blocks.
 * 
 */

#include "../uthash/uthash.h"
#include "../uthash/utlist.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include "../include/sthread.h"
#include "../include/block.h"
#include "../include/cache.h"

#define FALSE 0
#define TRUE (!FALSE)
#define CACHE_SIZE 30
#define CACHE_BLOCK_SIZE 512

/*
 *
ini
destroy
create
get_attr
readdir
open
read
write
truncate
 */

/*
 * author rui
 * 	A cache usa as seguintes funcoes para aceder aos blocos
 *
 #define BLOCK_READ(bks,block_no,block)			block_read(bks,block_no,block)
 #define BLOCK_WRITE(bks,block_no,block)			block_write(bks,block_no,block)
 */

typedef struct note_t node;

node* new_node(int block_no);
node *find_node(int block_no);

/**
 * Estrutura que guarda o estado de um bloco que se encontra em cache
 */
struct note_t {
	int block_no;
	int cache_no;
	int clean; //boolean
	UT_hash_handle hh;
	node *prev;
	node *next;
};

// hash table
static node *hash;
// clean double linked list
static node *clean_head;

// dirty double linked list
static node *dirty_head;
// DL list, contains the free positions on the cache_memory


static int cache_index;


static int num_hit;
static int num_access;
static int misses;

static int flushed;

// the array of blocks that are kept in cache
/**
 * Vector que contem os blocos em cache, tem tamalho limitado definido
 * pelo parametro cache_dim, indica quantos blocos podem estar simultaneamente
 * em cache
 */

static DISK_TYPE* cache_memory;

/**
 * used for temporarily store a block
 */
char* BLOCK;

/**
 * hdd
 *
 */
static DISK_TYPE* HDD;

/**
 * Numero de blocos em cache
 */
static int num_cache;

/**
 *
 */

sthread_mutex_t mutex;
/**
 * DL_PREPEND(head,add)
 * DL_APPEND(head,add)
 * DL_DELETE(head,del)
 *
 */

void updateList(node* s) {
	if (s->clean == TRUE) {
		DL_DELETE(clean_head,s);
		DL_APPEND(clean_head,s);
	} else {
		DL_DELETE(dirty_head, s);
		DL_APPEND(dirty_head, s);
	}
}

int readFromDisk(node* s, char * block) {
	int result;
	result = BLOCK_READ(HDD, s->block_no, block);
	result |= BLOCK_WRITE(cache_memory, s->cache_no, block);
	if (s->clean == TRUE) {
		DL_DELETE(clean_head,s);
	} else {
		DL_DELETE(dirty_head, s);
	}
	s->clean = TRUE;
	DL_APPEND(clean_head, s);

	return result;
}
int readFromCache(node* s, char* block) {
	int result;
	result = BLOCK_READ(cache_memory, s->cache_no , block ); //replace block_no for block_no_to_id(block_no)
	updateList(s);
	return result;
}

int writeToCache(node* s, char* block) {
	int result;
	result = BLOCK_WRITE(cache_memory, s->cache_no,block);
	if (s->clean == TRUE) {
		DL_DELETE(clean_head,s);
	} else {
		DL_DELETE(dirty_head, s);
	}
	s->clean = FALSE;
	DL_APPEND(dirty_head, s);
	return result;
}

/**
 *
 */
int copyToDisk(node* s) {
	++flushed;
	int result;
	result = BLOCK_READ(cache_memory, s->cache_no, BLOCK);
	result = BLOCK_WRITE(HDD, s->block_no, BLOCK);
	if (s->clean == TRUE) {
		DL_DELETE(clean_head,s);
	} else {
		DL_DELETE(dirty_head, s);
	}
	s->clean = TRUE;
	DL_APPEND(clean_head, s);
	return result;
}

/**
 * 	cria uma nova node, se a cache esta cheia retira uma node por LRU
 *
 * 	return uma node inicializada que esta clean, e esta na lista clean
 *
 *
 */
node* new_node(int block_no) {
	int result;
	node* n = malloc(sizeof(node));
	n->block_no = block_no;

	n->clean = TRUE;
	n->next = NULL;
	n->prev = NULL;

	if (num_cache == CACHE_SIZE) { //a cache ja nao tem espaco
		node* del; //node a ser eleminada
		// retira a node correspondente ao ultimo bloco acedido
		if (clean_head != NULL) { //se a lista dos registos limpos nao estiver vazia
			del = clean_head;
			DL_DELETE(clean_head, clean_head);
		} else {
			del = dirty_head;
			DL_DELETE(dirty_head,dirty_head);
			result = BLOCK_READ(cache_memory, del->cache_no, BLOCK);
			result |= BLOCK_WRITE(HDD, n->block_no, BLOCK);
			flushed++;
		}


		HASH_DEL( hash, del);
		n->cache_no = del->cache_no;
		free(del);

	} else {

		n->cache_no = num_cache;
		num_cache++;
	}


	HASH_ADD_INT( hash, block_no, n);
	DL_APPEND(clean_head, n);
	return n;
}

/**
 * 	Adiciona a node à hash
 *
 * 	param n:
 * 	A node a inserir
 *
 */

void add_node(node* n) {
	//block_no is the node field
	HASH_ADD_INT( hash, block_no, n);
}

/**
 * Elimina o bloco dado da hash
 *
 * param n:
 * A node a eliminar do hash
 */



/**
 * procura na hash o node do bloco_no
 *
 * param block_no:
 * o numero do bloco
 * return:
 * A node associada ao bloco, NULL se não existir
 */

node *find_node(int block_no) {
	node *s;
	HASH_FIND_INT( hash, &block_no, s);
	/* s: output pointer */
	return s;
}

/*
 * cache_init: initialize block/register cache
 * - allocator: the blocks/registers instance to be cached
 *
 */
void cache_init(DISK_TYPE* allocator) {
	mutex = sthread_mutex_init();
	HDD = allocator;
	hash = NULL;
	clean_head = NULL;
	dirty_head = NULL;
	num_hit = 0;
	num_access = 0;
	flushed = 0;
	misses = 0;
	cache_index = 0;
	BLOCK = malloc(sizeof(char) * CACHE_BLOCK_SIZE);
	cache_memory = DISK_INIT(CACHE_SIZE, CACHE_BLOCK_SIZE);

	num_cache = 0;

	return;
}

/* A cache verifica se ja tem o block block_no, se sim serve imediatamente o pedido,
 * senao obtem o bloco do sistem da ficheiros, BLOCK_READ, guarda o block na hash e
 * serve o pedido
 *
 * cache_read: read a whole block/register
 * - no: the number of the block/register to re
ad
 * - block: the buffer were to copy the block/register 
 *   returns: 0 if sucessful, -1 if not
 */
int cache_read(unsigned block_no, char* block) {
	sthread_mutex_lock(mutex);
	num_access++;
	int result = 0;
	node *s;
	if ((s = find_node(block_no)) != NULL) {
		++num_hit;
		result = readFromCache(s, block);
	} else {
		++misses;
		s = new_node(block_no);
		result = readFromDisk(s, block);
	}
	sthread_mutex_unlock(mutex);
	return result;
}

/*
 * * Verifica se ja tem o bloco
 *
 * Se sim, escreve no block e marca-o como sujo
 * Se nao cria uma nova entrada na cache com o conteudo
 * a escrever e marca-a como sujo
 *
 * cache_write: write a whole block/register
 * - no: the number of the block/register to write
 * - block: the data with the data to write
 *   returns: 0 if sucessful, -1 if not
 */
int cache_write(unsigned block_no, char* block) {
	sthread_mutex_lock(mutex);
	num_access++;
	node* p = find_node(block_no);
	int result;
	//Verifica se ja tem o bloco
	if (p == NULL) {
		++misses;
		//reserva espaço em cache
		p = new_node(block_no);
	}else{
		++num_hit;
	}
	result = writeToCache(p, block);
	sthread_mutex_unlock(mutex);
	return result;
}

/*
 * cache_block_flush: forces flush of any dirty cache block/register
 */
void cache_flush() {
	sthread_mutex_lock(mutex);

//Quando o sofs é unmounted, todos os blocos/registos dirty em cache devem ser escritos para o sistema
//de ficheiros, sequencialmente (ou seja, um a seguir ao outro).
	node *elt, *tmp;

	/* now delete each element, use the safe iterator */
	DL_FOREACH_SAFE(dirty_head, elt, tmp)
	{
		copyToDisk(elt);
	}

	sthread_mutex_unlock(mutex);
	return;
}

int cmp(node* a, node*b){
	if( a->block_no < b->block_no)
		return -1;
	if(  a->block_no > b->block_no)
		return 1;
	else
		return 0;
}
/*
 * cache_dump_stats: display statics of the cache
 */
void cache_dump_stats() {
	sthread_mutex_lock(mutex);
	node* elt;
	printf("Capacidade: %d\n", CACHE_SIZE);
	printf("Clean blocks/registers:");
	// <lista do número de cada bloco/registo limpo em cache>
	DL_SORT(clean_head,cmp);
	DL_FOREACH(clean_head, elt)
	{
		printf("%d ", elt->block_no);
	}
	printf("\n");
	DL_SORT(dirty_head,cmp);
	printf("Dirty blocks/registers: ");
	//<lista do número de cada bloco/registo sujo em cache>
	DL_FOREACH(dirty_head, elt)
	{
		printf("%d ", elt->block_no);
	}
	printf("\n");
	//<nº de hits/nº total de acessos>
	double hit_racio = (num_hit*1.0)/(1.0*num_access);
	printf("Hit ratio: %f \n", hit_racio);
	//<nº total de faltas de cache (misses)>
	printf("Misses: %d \n", misses);
	//<nº total de blocos/registos sujos que foram flushed para FS>
	printf("Flushed: %d \n", flushed);
	sthread_mutex_unlock(mutex);
	return;
}

